package megamu.mesh;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;

import jellyfishumbrella.tikis.Volcano;

import com.jellyfishumbrella.GL.AbstractLOD;
import com.jellyfishumbrella.GL.MeshGL;
import com.jellyfishumbrella.GL.Normal;
import com.jellyfishumbrella.GL.Polygon;
import com.jellyfishumbrella.GL.UberPoly;
import com.jellyfishumbrella.GL.Vector2d;
import com.jellyfishumbrella.GL.Vector3d;
import com.jellyfishumbrella.GL.Vertex;
import com.jellyfishumbrella.util.Trace;

/**
 * TODO Put here a description of what this class does.
 * 
 * @author 500441547. Created Sep 14, 2012.
 */
public class Region extends UberPoly {
	/**
	 * TODO Put here a description of this field.
	 */
	public Site						site;
	private double					moundHeight				= 0;
	private LinkedHashSet<Corner>	corners					= new LinkedHashSet<Corner>();
	private LinkedHashSet<Edge>		edges					= new LinkedHashSet<Edge>();
	/**
	 * TODO Put here a description of this field.
	 */
	public LinkedHashSet<Region>	neighbors				= new LinkedHashSet<Region>();

	private CreateGLpolygonThread	createGLpolygonThread	= new CreateGLpolygonThread(true);
	
	public boolean windCW = true;

	// ==================================================================================

	public double getMoundHeight() {
		return this.moundHeight;
	}

	public void setMoundHeight(double moundHeight) {
		this.moundHeight = Math.max(0, moundHeight);

		if (this.moundHeight > 0) {
			this.setSurface("lava");
		} else {
			this.setSurface("beach");
		}
	}

	// ==================================================================================

	/**
	 * TODO Put here a description of what this constructor does.
	 */
	public Region() {
		this(0);
	}

	/**
	 * TODO Put here a description of what this constructor does.
	 * 
	 * @param points
	 */
	public Region(int points) {
		super();

		setSurface("ocean");

		this.corners = new LinkedHashSet<Corner>();
		this.edges = new LinkedHashSet<Edge>();
		this.neighbors = new LinkedHashSet<Region>();
		this.maxLOD = 2;
	}

	public Region(Site newSite) {
		this(0);
		this.setSite(newSite);
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @param x
	 * @param z
	 */
	public void addCorner(float x, float z) {
		Corner newCorner = new Corner(x, 0, z);
		newCorner.addRegion(this);
		this.corners.add(newCorner);
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @param corner
	 */
	public void addCorner(Corner corner) {
		if (!corners.contains(corner)) {
			corner.addRegion(this);
			this.corners.add(corner);
		}
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @param edge
	 */
	public void addEdge(Edge edge) {
		this.edges.add(edge);
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @return count
	 */
	public int vertCount() {
		return this.corners.size();
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @return corners
	 */
	public ArrayList<Corner> getCorners() {
		ArrayList<Corner> corners2 = new ArrayList<Corner>(this.corners);

		// for(Corner corner : corners){
		// corners2.add(corner);
		// }
		return corners2;
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @return verts
	 */
	public ArrayList<Vertex> getVerts() {
		ArrayList<Vertex> verts = new ArrayList<Vertex>();

		ArrayList<Corner> corners2 = new ArrayList<Corner>(this.corners);

		for (Corner corner : corners2) {
			verts.add(corner.vert);
			if(corner.vert.getIndex() == 0){
				System.out.println(corner.vert.getIndex());
			}
		}
		return verts;
	}

	@Override
	public String toString() {
		String str = "MPolygon: ";
		for (Corner corner : this.corners) {
			str += corner.toString();
		}
		return str;
	}

	/**
	 * TODO Put here a description of what this method does.
	 */
	public void pointNormalsUp() {
		alignNormals(new Vector3d(0, 1, 0));
	}

	public void alignNormals(Vector3d norm) {
		ArrayList<Corner> corners2 = new ArrayList<Corner>(this.corners);
		if (corners2.size() >= 3) {
			Normal normal = computeTriangleNormal(corners2.get(0), corners2.get(1));
			double dotProduct = normal.x * norm.x + normal.y * norm.y + normal.z * norm.z;
			//System.out.println("normals:   " + corners2.get(0) + "," + corners2.get(1) + "," + dotProduct);
			if (dotProduct > 0) {
				Collections.reverse(corners2);
			}
		}
		this.corners = new LinkedHashSet<Corner>(corners2);
	}

	/**
	 * TODO Put here a description of what this method does.
	 * 
	 * @param corner1
	 * @param corner2
	 * @return normal
	 */
	public Normal computeTriangleNormal(Corner corner1, Corner corner2) {
		Normal a = new Normal();// normals
		Normal b = new Normal();// normals

		Normal normal = new Normal();
		normal.x = 0.0f;
		normal.y = 0.0f;
		normal.z = 0.0f;

		a.x = corner1.vert.x() - this.site.vert.x();
		a.y = corner1.vert.y() - this.site.vert.y();
		a.z = corner1.vert.z() - this.site.vert.z();

		b.x = corner2.vert.x() - this.site.vert.x();
		b.y = corner2.vert.y() - this.site.vert.y();
		b.z = corner2.vert.z() - this.site.vert.z();

		normal.x = (float) (b.y * a.z - b.z * a.y);
		normal.y = (float) (b.z * a.x - b.x * a.z);
		normal.z = (float) (b.x * a.y - b.y * a.x);

		normal.normalize();
		return normal;
	}

	/**
	 * TODO Put here a description of what this method does.
	 */
	public void computeNormal() {
		ArrayList<Corner> corners2 = new ArrayList<Corner>(this.corners);

		Normal avgNormal = new Normal();
		avgNormal.x = 0.0f;
		avgNormal.y = 0.0f;
		avgNormal.z = 0.0f;

		for (int i = 1; i < corners2.size() - 1; i++) {
			Normal normal = computeTriangleNormal(corners2.get(i), corners2.get(i + 1));

			avgNormal.x += normal.x;
			avgNormal.y += normal.y;
			avgNormal.z += normal.z;
		}

		avgNormal.x /= corners2.size();
		avgNormal.y /= corners2.size();
		avgNormal.z /= corners2.size();

		avgNormal.normalize();

		corners2.get(0).vert.normal = this.site.vert.normal = avgNormal;
	}

	@Override
	public synchronized LinkedHashSet<AbstractLOD> preSetLOD(int newLOD) {
		LinkedHashSet<AbstractLOD> needsUpdate = new LinkedHashSet<AbstractLOD>();
		if (newLOD >= 0) {
			if (newLOD != this.LOD || newLOD > this.nextLOD) {
				needsUpdate.add(this);
				this.nextLOD = newLOD;
			}
			Iterator<Region> itr = this.neighbors.iterator();
			while (itr.hasNext()) {
				Region currNeighbor = itr.next();
				needsUpdate.addAll(currNeighbor.preSetLOD(newLOD - 1));
			}
		}
		return needsUpdate;
	}

	@Override
	public synchronized void updateLOD() {
		setLOD(this.nextLOD);
	}

	@Override
	public int setLOD(int newLOD) {
		if (newLOD >= 0 && newLOD != this.LOD) {
			this.LOD = Math.min(this.maxLOD, newLOD);
			// Iterator itr = this.neighbors.iterator();
			// while (itr.hasNext()) {
			// Region currNeighbor = (Region) itr.next();
			// currNeighbor.setLOD(newLOD - 1);
			// }
			this.createGLpolygonThread.run();
			// createGLpolygonThread.start();
		}
		return this.LOD;
	}

	@Override
	public int incLOD() {
		if (this.LOD < this.maxLOD) {
			this.LOD++;
			new CreateGLpolygonThread(true);
		}
		return this.LOD;
	}

	@Override
	public int decLOD() {
		if (this.LOD > 0) {
			this.LOD--;
			new CreateGLpolygonThread(true);
		}
		return this.LOD;
	}

	/**
	 * TODO Put here a description of what this class does.
	 * 
	 * @author 500441547. Created Sep 14, 2012.
	 */
	public class CreateGLpolygonThread extends Thread {
		private boolean	immediateUpdate	= true;

		/**
		 * TODO Put here a description of what this constructor does.
		 * 
		 * @param surface
		 * @param immediateUpdate
		 */
		public CreateGLpolygonThread(boolean immediateUpdate) {
			this.immediateUpdate = immediateUpdate;
		}

		@Override
		public void run() {
			createGLpolygon(this.immediateUpdate);
		}

		/**
		 * TODO Put here a description of what this method does.
		 * 
		 * @param surface
		 * @param immediateUpdate
		 */
		public synchronized void createGLpolygon(boolean immediateUpdate) {
			// parentMesh.removePolygonGroup(Region.this);
			// polygons.clear();
			switch (Region.this.LOD) {
				case 0:
					createGLpolygonLOD0(Region.this.parentMesh);
					break;
				case 1:
					createGLpolygonLOD1(Region.this.parentMesh);
					break;
				case 2:
					createGLpolygonLOD2(Region.this.parentMesh);
					break;
			}
			if (immediateUpdate) {
				Region.this.parentMesh.refreshVerts = true;
				Region.this.parentMesh.refreshIndices = true;
			}
		}

		/**
		 * TODO Put here a description of what this method does.
		 * 
		 * @param surface
		 * @param parentMesh
		 */
		public synchronized void createGLpolygonLOD0(MeshGL parentMesh) {
			// ArrayList<Vertex> points = getVerts();
			Region.this.allPoints = getVerts();
			if(Region.this.windCW){
				Collections.reverse(Region.this.allPoints);
			}
			glPolys.add(parentMesh.addPolygonFan(Region.this.surface.name, Region.this, Region.this.allPoints, true));
		}

		/**
		 * TODO Put here a description of what this method does.
		 * 
		 * @param surface
		 * @param parentMesh
		 */
		public synchronized void createGLpolygonLOD1(MeshGL parentMesh) {
			ArrayList<Vertex> points = getVerts();
			if(Region.this.windCW){
				Collections.reverse(points);
			}
			glPolys = parentMesh.addBufferedPolygonFan("default", Region.this, points, 0.666, true);
			//glPolys = parentMesh.addBufferedPolygonFan(Region.this.surface.name, Region.this, points, 0.666, true);
// glPolys.addAll(parentMesh.extrude(Region.this, new Vector3d(0,-1,0)));
		}

		/**
		 * TODO Put here a description of what this method does.
		 * 
		 * @param surface
		 * @param parentMesh
		 */
		public synchronized void createGLpolygonLOD2(MeshGL parentMesh) {
			ArrayList<Vertex> points = getVerts();
			if(Region.this.windCW){
				Collections.reverse(points);
			}
			glPolys = parentMesh.addBufferedPolygonFan(Region.this.surface.name, Region.this, points, 0.666, true);
		}
	}

	public void translateVertexByType(Vector3d adj, UberPoly.VertexLODTag vertexType, double percent, Boolean immediateUpdate) {
		Trace.getCurrentMethodName();

		LinkedHashSet<Vertex> glVerts = new LinkedHashSet<Vertex>();

		for (Polygon currPoly : glPolys) {
			for (Vertex currVert : currPoly.verts) {
				if (currVert.vertexLODType == vertexType) {
					glVerts.add(currVert);
				}
			}
		}

		for (Vertex currVertex : glVerts) {
			currVertex.setX(currVertex.x() + adj.x * percent);
			currVertex.setY(currVertex.y() + adj.y * percent);
			currVertex.setZ(currVertex.z() + adj.z * percent);
		}

		adjustSurfaceByAltitude(glVerts);

		for (Polygon currPoly : glPolys) {
			currPoly.normal = parentMesh.calcPolyNormal(currPoly);
		}

		for (Vertex currVertex : glVerts) {
			parentMesh.adjVertexVBO(currVertex);
		}

		this.parentMesh.reUploadVerts = immediateUpdate;
		// this.parentMesh.refreshVerts = true;
		this.parentMesh.refreshIndices = immediateUpdate;
	}

	public void smoothOUTERpoints(Boolean immediateUpdate) {
		Trace.getCurrentMethodName();

		LinkedHashSet<Vertex> glVerts = new LinkedHashSet<Vertex>();

		for (Polygon currPoly : glPolys) {
			for (Vertex currVert : currPoly.verts) {
				if (currVert.vertexLODType == UberPoly.VertexLODTag.OUTER) {
					glVerts.add(currVert);
				}
			}
		}

		for (Vertex currVert : glVerts) {
			Vector3d avg = new Vector3d();
			Iterator itr = currVert.connectedUberPolys.iterator();
			while (itr.hasNext()) {
				Region currRegion = (Region) itr.next();
				avg.x += currRegion.site.vert.x();
				avg.y += currRegion.site.vert.y();
				avg.z += currRegion.site.vert.z();
			}
			avg.scale(1.0 / currVert.connectedUberPolys.size());

			currVert.setX(avg.x);
			currVert.setY(avg.y);
			currVert.setZ(avg.z);
		}

		adjustSurfaceByAltitude(glVerts);

		for (Polygon currPoly : glPolys) {
			currPoly.normal = parentMesh.calcPolyNormal(currPoly);
		}

		for (Vertex currVertex : glVerts) {
			parentMesh.adjVertexVBO(currVertex);
		}

		this.parentMesh.reUploadVerts = immediateUpdate;
		// this.parentMesh.refreshVerts = true;
		this.parentMesh.refreshIndices = immediateUpdate;
	}

	public synchronized void adjustSurfaceByAltitude(LinkedHashSet<Vertex> glVerts) {
		Trace.getCurrentMethodName();
		// this.setSurface("ocean");
		/*
		 * LinkedHashSet<Vertex> glVerts = new LinkedHashSet<Vertex>(); for (Polygon currPoly : this.glPolys) { for (Vertex currVert : currPoly.verts) { glVerts.add(currVert); } }
		 */
		for (Vertex currVert : glVerts) {
			currVert.uv = new Vector2d(0, (currVert.y() - Volcano.MIN_ALT) / (Volcano.MAX_ALT - Volcano.MIN_ALT));

			if (currVert.y() > 0) {
				this.setSurface("beach");
			}
		}
	}

	public void translate(Vector3d adj, Boolean immediateUpdate) {
		Trace.getCurrentMethodName();

		this.site.vert.setX(this.site.vert.x() + adj.x);
		this.site.vert.setY(this.site.vert.y() + adj.y);
		this.site.vert.setZ(this.site.vert.z() + adj.z);

		translateVertexByType(adj, UberPoly.VertexLODTag.OUTER, 1.0, false);
		/*
		 * LinkedHashSet<Vertex> glVerts = new LinkedHashSet<Vertex>(); for(Polygon currPoly : glPolys){ for(Vertex currVert : currPoly.verts){ glVerts.add(currVert); } } for(Vertex currVertex : glVerts){ currVertex.x += adj.x; currVertex.y += adj.y; currVertex.z += adj.z; parentMesh.adjVertexVBO(currVertex); }
		 */

		if (immediateUpdate) {
			this.parentMesh.reUploadVerts = true;
			// this.parentMesh.refreshVerts = true;
			// this.parentMesh.refreshIndices = true;
		}
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((site == null) ? 0 : site.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Region other = (Region) obj;
		if (site == null) {
			if (other.site != null)
				return false;
		} else if (!site.equals(other.site))
			return false;
		return true;
	}

	public void setSite(Site newSite) {
		this.site = newSite;
		newSite.region = this;
	}

	public void addNeighbor(Region currRegion1) {
		neighbors.add(currRegion1);
		currRegion1.neighbors.add(this);
	}
}
